﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Linq.Expressions;
using System.Threading.Tasks;
using Codeflows.Results;

namespace Codeflows.Dispatch
{
    class ActionMethodBuilder
    {
        static readonly object Lock = new object();

// ReSharper disable StaticFieldInGenericType
        // This static field is intentional.
        // We need a cache of ActionMethodDescriptors for each type.
        static readonly Dictionary<Type, ActionMethodDescriptor[]> Actions = 
            new Dictionary<Type, ActionMethodDescriptor[]>();
// ReSharper restore StaticFieldInGenericType
        
        static void PrepareActions(Type codeflowType)
        {
            const BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.Instance;

            var methodInfos = codeflowType.GetMethods(bindingFlags)
                                       .Where(mi => mi.ReturnType == typeof(Result) || 
                                                    mi.ReturnType == typeof(Task<Result>))
                                       .ToArray();
            
            var actions = new ActionMethodDescriptor[methodInfos.Length];

            for (int i = 0; i < methodInfos.Length; i++)
            {                
                var methodInfo = methodInfos[i];
                if (methodInfo.ReturnType == typeof(Result))
                    actions[i] = new SyncActionMethod(methodInfo, MakeCallSite(methodInfo));
                else
                    actions[i] = new AsyncActionMethod(methodInfo, MakeAsyncCallSite(methodInfo));
            }

            Actions[codeflowType] = actions;
        }

        static Func<object, ActionContext, Result> MakeCallSite(MethodInfo methodInfo)
        {
            // Makes the following expressions
            // i, a => (UserCodeflow)i.Xxx(a.ResolveValue(argumentName, argumentType)
            var i = Expression.Parameter(typeof(object), "i");
            var a = Expression.Parameter(typeof(ActionContext), "a");

            var expr = Expression.Lambda<Func<object, ActionContext, Result>>(
                Expression.Call(Expression.Convert(i, methodInfo.DeclaringType), methodInfo, BuildArgumentResolutionExpressions(methodInfo, a)), i, a);

            return expr.Compile();
        }

        static Func<object, ActionContext, Task<Result>> MakeAsyncCallSite(MethodInfo methodInfo)
        {
            // Makes the following expressions
            // i, a => i.Xxx(a.ResolveValue(argumentName, argumentType)
            var i = Expression.Parameter(typeof(object), "i");
            var a = Expression.Parameter(typeof(ActionContext), "a");

            var expr = Expression.Lambda<Func<object, ActionContext, Task<Result>>>(
                Expression.Call(Expression.Convert(i, methodInfo.DeclaringType), methodInfo, BuildArgumentResolutionExpressions(methodInfo, a)), i, a);

            return expr.Compile();
        }

        static IEnumerable<Expression> BuildArgumentResolutionExpressions(MethodInfo targetMethodInfo, Expression resolverInstance)
        {
            return targetMethodInfo.GetParameters().Select(
                pi => Expression.Convert(BuildArgumentResolutionCall(resolverInstance, pi), pi.ParameterType));
        }

        static Expression BuildArgumentResolutionCall(Expression resolverInstance, ParameterInfo targetParameter)
        {
            return Expression.Call(resolverInstance,
                ActionContext.ArgumentResolver,
                new[] 
                { 
                    Expression.Constant(targetParameter.Name), 
                    Expression.Constant(targetParameter.ParameterType.AssemblyQualifiedName) 
                });
        }

        public static ActionMethodDescriptor[] Build(Codeflow codeflow)
        {
            var type = codeflow.GetType();

            if (!Actions.ContainsKey(type))
            {
                lock(Lock)
                {
                    if (!Actions.ContainsKey(type))
                    {
                        PrepareActions(type);        
                    }        
                }
            }

            return Actions[type];
        }
    }
}
